Younix's Studio.

Linux 中的互斥机制(中断屏蔽、原子操作、自旋锁、信号量)

字数统计: 2.6k阅读时长: 9 min
2015/12/31 Share

基本概念

临界区

对某段代码而言,可能会在程序中多次被执行,每次执行的过程我们称作代码的执行路径。
当两个或多个代码路径要竞争共同的资源的时候,该代码段就是临界区。

互斥机制

访问共享资源的代码叫做临界区。共享资源被多个线程需要,但共享资源又不能被同时访问。
所以临界区需要以某种互斥机制来加以保护,确保共享资源被互斥访问。

用户空间和内核空间

为了安全考虑,Linux系统分为内核态和用户态,分别运行在内核空间和用户空间
内核态的程序可以执行特权指令,操作系统本身也在其中运行;
用户态则不允许直接访问操作系统的核心数据、设备等关键资源,必须先通过系统调用或者中断进入内核态才可以访问,当系统调用或中断返回时,重新回到用户空间运行。

Linux 的互斥机制

四种方式:中断屏蔽、原子操作、自旋锁、信号量

内核空间互斥方式:中断屏蔽、原子操作、自旋锁
用户空间互斥方式:信号量

中断屏蔽

中断是一个完全异步的事件,它的发生与正在运行的进程没有任何关系,它没有进程上下文切换
CPU具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序抢占,防止竞态的产生。
但是,内核的正常运行依赖于中断机制。在屏蔽中断期间,任何中断都无法得到处理,而必须等待屏蔽解除。因此长时间屏蔽中断对内核的运行起到很大的影响,其后果可能导致数据丢失,甚至系统崩溃。
实际情况是:在中断服务全过程屏蔽中断会丢失中断;如果开中断,又容易引起互斥问题。
为了解决这个问题,Linux 把中断分为顶半部TH(Top Half)和底半部BH(Bottom Half)。
TH 屏蔽中断,执行一些少量的关键性动作;BH 可以开中断,允许中断延迟执行。

原子操作

原子操作底层表现为一条汇编指令(ldrex、strex)。所以他们在执行过程中不会被别的代码路径所中断。
Linux 内核提供了两类函数来实现内核中的原子操作,分别是整型原子操作位原子操作。它们的共同点是所有的操作都是原子的,内核可以安全的调用它们而不被中断,而且它们都依赖底层CPU的原子操作实现,因此所有的这些函数都是与CPU架构相关的。

自旋锁

自旋锁是为实现保护共享资源而提出一种锁机制。

自旋锁的原理:
一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,并且在任何时刻最多只能有一个执行单元获得锁;
而在访问完共享资源后,必须释放锁。
如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;
如果在获取自旋锁时锁已经有保持者,那么获取锁操作将一直循环在那里,直到该自旋锁的保持者释放了锁,”自旋”一词就是因此而得名。

事实上,自旋锁的初衷是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁被持有的时间不应该过长。如果需要长时间锁定的话, 最好使用信号量。

信号量

在用户空间只有进程的概念。当一个临界区有多个用户态进程竞争时,最好的方法是用信号量保护这个临界区。
只有得到信号量进程才能执行临界区代码,当获取不到信号量时,进程进入休眠状态。

因此,我们可以说,信号量是进程级的互斥机制,它代表进程来争夺共享资源,如果竞争失败,就会发生进程上下文切换,当前进程进入睡眠状态,CPU运行其他进程。由于进程上下文切换的开销很大,因此,只有当进程占用资源时间较长时,用信号量才是最好的选择。

此外,信号量在SMP(对称多处理器)系统同样起作用。

浅显的比方

浅显的来说,可以理解为大家在一套房子里合租,共用一个厕所。厕所就是共享资源,去上厕所的行为被称作代码路径。
中断屏蔽就是,有一个人想要用厕所,但是呢他在上厕所前在门口贴上纸条说厕所坏了,如果他很快出来倒还不要紧,但是如果他要上很长时间,那一起住的其他人可能就要憋爆了。所以中断屏蔽最开始不会被用于处理需要耗时很长的操作。但是大家想,这样不是个解决办法啊,我有时候确实要拉很长时间怎么办呢。
于是发明了顶半部TH和底半部BH,TH用于执行少量的关键性的动作,BH用于处理中断中耗时的部分。
可以理解为,某人A特别特别想上厕所的时候,就进入TH(可以看作一个状态),此时A去应个急,拉一点点,让肚子不那么疼,此时A是不可以被打断的。如果没人用厕所他就直接慢条斯理的开始拉了(BH)。但是他这个BH状态是可以打断的,如果此时来个人B 非常非常急,B进入TH说,我受不了啦要憋死啦,A就会暂停自己的状态(保护现场)让B进来拉一会(TH),等B拉了一点点,让肚子不那么疼了就出去。此时 A继续(恢复现场)。等A 的BH部分完全结束后B再执行B的BH部分。
原子操作很好理解,就是大家每次上厕所都用时非常短,短到什么程度呢,只要一条汇编指令的时间。当然拉的量也非常少(只改变一个整型或者是位)。所以就不存在抢厕所的问题了。
自旋锁顾名思义,给这个厕所上把锁,只有拥有这个锁钥匙的人A才能进厕所。进去后把锁锁上,外面的人B急得团团转(自旋),出来后把锁释放,在门口等着的B拿了钥匙赶紧开了锁进去了。但是缺点就是,B在外面团团转,没有功夫去做别的事情,所以一旦 A 上厕所的时间很长,B就浪费了很长时间在自旋上。对系统的性能有所影响。
信号量信号量就是,我们的房子有 N 个厕所,N 不为 1, 且 N 为有限个,上厕所的人是有限的。即共享这一块资源的进程是有限个数的。这时候我们就可以在厕所门口挂上 N 吧钥匙,拿到钥匙的就可以进去,钥匙架空了,其他进程就只能在门口等待出来的人还钥匙。

区别分析

代码实现

中断屏蔽

1
2
3
local_irq_disable() /local_irq_save(flags);
// 访问临界区
local_irq_enable() /local_irq_restore(flags);

原子操作

位原子操作:set_bit/clear_bit/change_bit/test_bit
整型原子操作:atomic_set/atomic_read/atomic_add/atomic_sub/atomic_inc/atomic_dec/
步骤:

1
2
3
4
//1. 分配整形原子变量
atomic_t v = ATOMIC_INIT(1);
//2.操作原子变量
atomic_set/atomic_read/atomic_add/atomic_sub/atomic_inc加加/atomic_dec减减/...

实例:

1
2
static int open_cnt = 1;
open_cnt++; //不具备原子性

方法一,中断屏蔽,不适用于多核

1
2
3
4
unsigned long flags;
local_irq_save(flags);
open_cnt++;
local_irq_restore(flags);

方法二,原子操作

1
2
static atomic_t open_cnt = ATOMIC_INIT(1);
atomic_inc(&open_cnt); //具有原子性

自旋锁

1
2
3
4
5
6
7
8
9
10
11
//1.分配自旋锁
spinlock_t lock;
//2.初始化自旋锁
spin_lock_init(&lock);
//3.访问临界区之前获取锁:
spin_lock(&lock); //获取自旋锁,立即返回,如果没有获取锁,将进行忙等待
或者
spin_trylock(&lock); //获取锁,返回true,否则返回false,所以这个函数一定要对返回值进行判断!
//4 .访问临界区
//5.释放自旋锁
spin_unlock(&lock);

衍生自旋锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//1.分配自旋锁
spinlock_t lock;
//2.初始化自旋锁
spin_lock_init(&lock);
//3.访问临界区前获取锁:
unsigned long flags;
spin_lock_irq(&lock); // = spin_lock() + local_irq_disable()
或者
spin_lock_irqsave(&lock, flags); // = spin_lock() local_irq_save()
//4.访问临界区
//5.释放自旋锁
spin_unlock_irq(&lock); // = spin_unlock()+ local_irq_enable()
或者
spin_unlock_irqrestore(&lock, flags); // = spin_unlock() + local_irq_restore()

信号量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1.分配信号量对象
struct semaphore sema;
//2.初始化为互斥信号量
init_MUTEX(&sema);
或者:
DECLARE_MUTEX(sema);
//3.访问临界区之前获取信号量
down(&sema);
//如果获取信号量,立即返回
//如果信号量不可用,进程将在此休眠,并且休眠的状态是 [ 不可中断的休眠状态 TASK_UNINTERRUPTIBLE] !
或者
down_interruptible(&sema);
//如果信号量不可用,进程将进入 [ 可中断的休眠状态 TASK_INTERRUPTIBLE ],如果返回0表示正常获取信号,如果返回非0,表示接受到了信号
down_trylock();
//获取信号,如果信号量不可用,返回非0,如果信号量可用,返回0;不会引起休眠,可以在中断上下文使用。返回值也要做判断!
//4.访问临界区:临界区可以休眠
//5.释放信号量
up(&sema);
//不仅仅释放信号量,然后唤醒休眠的进程,让这个进程去获取信号量来访问临界区

参考文章:
Linux 互斥机制:http://www.cnblogs.com/jan5/articles/3351186.html
线程同步之详解自旋锁(windows平台):http://www.cnblogs.com/cposture/p/SpinLock.html 可以参考
读写自旋锁详解,第 1 部分:https://www.ibm.com/developerworks/cn/linux/l-cn-rwspinlock1/ 以自动机的观点阐述读写自旋锁的原理
多种自旋锁形式之间的区别:http://www.360doc.com/content/11/0302/14/3038654_97459411.shtml

CATALOG
  1. 1. 基本概念
    1. 1.1. 临界区
    2. 1.2. 互斥机制
    3. 1.3. 用户空间和内核空间
  2. 2. Linux 的互斥机制
    1. 2.1. 中断屏蔽
    2. 2.2. 原子操作
    3. 2.3. 自旋锁
    4. 2.4. 信号量
  3. 3. 浅显的比方
  4. 4. 区别分析
  5. 5. 代码实现
    1. 5.1. 中断屏蔽
    2. 5.2. 原子操作
    3. 5.3. 自旋锁
    4. 5.4. 衍生自旋锁
    5. 5.5. 信号量